use std::{fmt::Display, fs, path::PathBuf};

use anyhow::{Error, Ok, Result};
use sha1::{Digest, Sha1};

use crate::ahoben::BencodeEntity;

pub struct TorMeta {
    bencode: BencodeEntity,
    announce: String,
    info_hash: Vec<u8>,
    piece_length: u32,
    chunks_count: u32,
}

impl TryFrom<&[u8]> for TorMeta {
    type Error = Error;

    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
        let bencode = BencodeEntity::try_from(value)?;
        let mut info_hash = Vec::<u8>::new();
        let mut ann = String::new();
        let mut piece_length: u32 = 0;
        let mut chunks_count: u32 = 0;
        if let BencodeEntity::Dict(d) = &bencode {
            if let Some(BencodeEntity::String(announce)) = d.get("announce") {
                ann.push_str(&String::from_utf8_lossy(announce.as_slice()));
            }
            if let Some(info_dict) = d.get("info") {
                let mut sha1 = Sha1::new();
                sha1.update(info_dict.encode());
                info_hash = sha1.finalize().to_vec();
                if let BencodeEntity::Dict(info_dict) = info_dict {
                    if let Some(BencodeEntity::Integer(length)) = info_dict.get("piece length") {
                        piece_length = *length as u32;
                    }
                    if let Some(BencodeEntity::String(s)) = info_dict.get("pieces") {
                        chunks_count = (s.len() / 20) as u32;
                    }
                }
                
            }
        }
        Ok(Self {
            bencode,
            announce: ann,
            info_hash,
            piece_length,
            chunks_count
        })
    }
}
#[derive(Debug)]
pub struct DownloadableFile {
    pub name: String,
    pub size: u64,
}

impl Display for DownloadableFile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "file name: {}; length: {}B", self.name, self.size)
    }
}

impl TorMeta {
    pub fn files(&self) -> Vec<DownloadableFile> {
        let mut vec = Vec::<DownloadableFile>::new();
        if let BencodeEntity::Dict(d) = &self.bencode {
            if let Some(BencodeEntity::Dict(info)) = d.get("info") {
                if let Some(BencodeEntity::List(file_list)) = info.get("files") {
                    for x in file_list {
                        if let BencodeEntity::Dict(file_item) = x {
                            let mut name: String = String::new();
                            let mut size = 0;
                            if let Some(BencodeEntity::List(l)) = file_item.get("path") {
                                let mut paths = Vec::<String>::new();
                                for item in l {
                                    if let BencodeEntity::String(i) = item {
                                        paths.push(String::from_utf8_lossy(i.as_slice()).into());
                                    }
                                }
                                name.push_str(&paths.join("/"));
                            }
                            if let Some(BencodeEntity::Integer(s)) = file_item.get("length") {
                                size = *s;
                            }
                            vec.push(DownloadableFile {
                                name,
                                size: size as u64,
                            })
                        }
                    }
                } else {
                    if let Some(BencodeEntity::String(file_name)) = info.get("name") {
                        if let Some(BencodeEntity::Integer(file_length)) = info.get("length") {
                            vec.push(DownloadableFile {
                                name: String::from_utf8_lossy(&file_name).into_owned(),
                                size: *file_length as u64,
                            })
                        }
                    }
                }
            }
        }
        vec
    }
    pub fn from(path: PathBuf) -> Result<Self> {
        let data = fs::read(path)?;
        let res = Self::try_from(data.as_slice())?;
        Ok(res)
    }

    pub fn get_announce<'a>(&'a self) -> &'a str {
        &self.announce
    }

    pub fn get_hashs<'a>(&'a self) -> Option<&'a [u8]> {
        if let BencodeEntity::Dict(d) = &self.bencode {
            if let Some(BencodeEntity::Dict(info)) = d.get("info") {
                if let Some(BencodeEntity::String(pices)) = info.get("pieces") {
                    return Some(pices);
                }
            }
        }
        None
    }

    pub fn get_chunk_hash<'a>(&'a self, idx: usize) -> Option<&'a [u8]> {
        if let Some(hashs) = self.get_hashs() {
            return Some(&hashs[(idx * 20)..((idx + 1) * 20)]);
        }
        None
    }

    pub fn get_info_hash(&self) -> &Vec<u8> {
        &self.info_hash
    }

    pub fn get_info_hash_str(&self) -> String {
        self.info_hash
            .iter()
            .map(|b| format!("{:02x}", b))
            .collect::<Vec<String>>()
            .join("")
    }

    pub fn get_size(&self) -> usize {
        if let BencodeEntity::Dict(d) = &self.bencode {
            if let Some(BencodeEntity::Dict(info)) = d.get("info") {
                if let Some(BencodeEntity::Integer(length)) = info.get("length") {
                    return *length as usize;
                }
            }
        }
        return 0;
    }
    pub fn get_piece_length(&self) -> u32 {
        self.piece_length
    }
    pub fn get_chunks_count(&self) -> u32 {
        self.chunks_count
    }

}

#[cfg(test)]
mod test {
    use super::TorMeta;

    #[test]
    fn test_file() {
        let tor = TorMeta::from("1.torrent".into()).unwrap();
        let files = tor.files();
        for file in files {
            println!("{}", file)
        }
    }
    #[test]
    fn test_ann() {
        let tor = TorMeta::from("1.torrent".into()).unwrap();
        let ann = tor.get_announce();
        println!("{}", ann);
    }

    #[test]
    fn test_hash() {
        let tor = TorMeta::from("1.torrent".into()).unwrap();
        let hash1 = tor.get_chunk_hash(1).unwrap();
        println!("{:?}", hash1);
    }

    #[test]
    fn test_info_hash() {
        let tor = TorMeta::from("ubuntu-24.04-live-server-amd64.iso.torrent".into()).unwrap();
        let info_hash = tor.get_info_hash_str();
        println!("{}", info_hash);
    }
}
